package util;

import java.math.BigInteger;

/**
 * This is a container class to store a curve. It offers some useful methods to read values
 * and calculate behavior between points on this curve.
 * @author Ralf
 *
 */
public class Curve {
	
	// Curve is: y^2 = x^3 + ax + b (mod p)
	private BigInteger a;
	private BigInteger b;
	private BigInteger p;
	
	/**
	 * Constructor with valid parameters for the function y^2 = x^3 + a*x + b (mod p)
	 * @param a scalar for x
	 * @param b absolute term of the function
	 * @param p prime number to set a range of the field
	 */
	public Curve(BigInteger a, BigInteger b, BigInteger p) {
		this.a = a;
		this.b = b;
		this.p = p;
	}
	
	/**
	 * Constructor using Strings as parameters.
	 * @see Curve(BigInteger a, BigInteger b, BigInteger p)
	 */
	public Curve(String a, String b, String p) {
		this(new BigInteger(a), new BigInteger(b), new BigInteger(p));
	}
		
	/**
	 * @return the a
	 */
	public BigInteger getA() {
		return a;
	}

	/**
	 * @return the b
	 */
	public BigInteger getB() {
		return b;
	}

	/**
	 * @return the p
	 */
	public BigInteger getP() {
		return p;
	}

	/**
	 * Addition of two points, which are presumed to be on this curve.
	 * @param p a point presumed on this curve
	 * @param q another point presumed on this curve
	 * @return the resulting point when adding the given points
	 */
	public Point addPoints(Point p, Point q) {
		// both points are PointOfInfinity, which doesn't change anything
		if (p.isPointOfInfinity() && q.isPointOfInfinity())
			return new Point();
		
		// only one point is PointOfInfity, so the result is the valid point
		if (p.isPointOfInfinity())
			return new Point(q);
		
		if (q.isPointOfInfinity())
			return new Point(p);
		
		// points are negative
		if (p.isModulusNegative(q, this.p))
			return new Point();
		
		// calculate sum of the two points
		// the calculation of s differs for equal and distinct P and Q
		BigInteger px, py, qx, qy, dividend, divisor, s, rx, ry;
		px = p.getX().mod(this.p);
		py = p.getY().mod(this.p);
		qx = q.getX().mod(this.p);
		qy = q.getY().mod(this.p);
				
		// points are equal
		if (p.isModulusEqual(q, this.p)) {
			// y equals 0
			if (p.getY().equals(BigInteger.ZERO))
				return new Point();
			
			// y inequals 0
			// use different formula for s
			// s = (3*px^2 + a)/(2*py)
			dividend = px.pow(2).multiply(BigInteger.valueOf(3)).add(a);
			divisor = py.multiply(BigInteger.valueOf(2));
			
		} else {		
			// points are distinct
			// use regular formula for s
			// s = (py - qy)/(px - qx)
			dividend = py.subtract(qy);
			divisor = px.subtract(qx);
		}

		s = Algorithms.remainderlessDivision(dividend, divisor, this.p);
		// use the formula for the new point using s
		// rx = s^2 - px - qx
		// ry = s(px - rx) - py
		rx = s.pow(2).subtract(px).subtract(qx);
		ry = s.multiply(px.subtract(rx)).subtract(py);
		
		rx = rx.mod(this.p);
		ry = ry.mod(this.p);
		return new Point(rx, ry);
	}
	
	/**
	 * Subtracts one point from another and returns the result. The original points remain unchanged.
	 * @param p Minuend
	 * @param q Subtrahend
	 * @return p-q
	 */
	public Point subtractPoints(Point p, Point q) {
		// both points are PointOfInfinity, which doesn't change anything
		if (p.isPointOfInfinity() && q.isPointOfInfinity())
			return new Point();
		
		// only one point is PointOfInfity, so the result is the valid point
		if (p.isPointOfInfinity())
			return new Point(q.getNegative());
		
		if (q.isPointOfInfinity())
			return new Point(p.getNegative());
		
		return addPoints(p, q.getNegative());
	}
	
	/**
	 * Scales a given point by adding it to itself scale-many times.
	 * Some algorithmic improvements are used to save time.
	 * @param p the point to scale
	 * @param scale how often the point is added to itself. must be greater than zero
	 * @return the scaled point
	 */
	public Point scalePoint(Point p, BigInteger scale) {
		if (scale.signum() <= 0)
			throw new IllegalArgumentException("scale <= 0");
		
		// if the scale is lower than 4, add the point to itself accordingly times
		if (scale.compareTo(new BigInteger("4")) < 0) {
			int tmp = scale.intValue();
			Point result = new Point(p);
			// only add, if scale > 1
			for(int i = 1; i < tmp; i++) {
				result = this.addPoints(result, p);
			}
			return result;			
		}
		
		// improve performance by adding the result point to itself many times and add/subtract to get the scale
		// get the best integer approximation for the log2(scale)
		BigInteger logVal = Algorithms.bestIntegerLog2(scale);
		Point result = new Point(p);
		
		for(BigInteger i = BigInteger.ZERO; i.compareTo(logVal) < 0; i = i.add(BigInteger.ONE)) {
			result = this.addPoints(result, result);
		}
		BigInteger logValExp = Algorithms.exp2(logVal);
		
		if (logValExp.compareTo(scale) < 0) {
			// add remaining points value
			return this.addPoints(result, this.scalePoint(p, scale.subtract(logValExp)));
		} else if (logValExp.compareTo(scale) == 0) {
			// scale was a power of 2
			return result;
		} else {
			// subtract the too much values
			return this.subtractPoints(result, this.scalePoint(p, logValExp.subtract(scale)));
		}
	}
	
	/**
	 * Checks, if the given point is on this curve.
	 * The PointOfInfinity is not considered to be on the curve.
	 * @param p a point
	 * @return true, if the point is valid and on this curve
	 */
	public boolean isOnCurve(Point p) {
		if (p.isPointOfInfinity())
			return false;
		
		BigInteger y2 = p.getY().pow(2).mod(this.p);
		BigInteger x3 = p.getX().pow(3);
		BigInteger ax = p.getX().multiply(a);
		BigInteger tmp = x3.add(ax).add(b).mod(this.p);
		
		return y2.equals(tmp);		
	}

}
